#include "Subsystems/BlockBehaviors/SubsystemMetersBlockBehavior.h"
#include "Blocks/JackOLanternBlock.h"
#include "Subsystems/SubsystemTerrain.h"
#include "Subsystems/SubsystemWeather.h"
#include "Subsystems/SubsystemTimeOfDay.h"
#include "Managers/BlocksManager.h"

#include "Blocks/ThermometerBlock.h"
#include "Blocks/HygrometerBlock.h"
#include "Blocks/WaterBlock.h"
#include "Blocks/IceBlock.h"


namespace PocketSurvival
{

    SubsystemMetersBlockBehavior::SubsystemMetersBlockBehavior()
    {
        m_handleBlocks.push_back(ThermometerBlock::Index());
        m_handleBlocks.push_back(HygrometerBlock::Index());
    }

    void SubsystemMetersBlockBehavior::onNeighborBlockChanged(int32_t x, int32_t y, int32_t z, int32_t neighborX, int32_t neighborY, int32_t neighborZ)
    {
        Point3 point = CellFace::FaceToPoint3(Terrain::ExtractData(m_terrain->getCellValue(x, y, z)));
        int32_t cellContents = m_terrain->getCellContents(x - point.posX, y - point.posY, z - point.posZ);
        if (GameSingleton::blocksManager->blocks[cellContents]->IsTransparent)
        {
            m_terrain->destroyCell(0, x, y, z, 0, false);
        }
    }

    void SubsystemMetersBlockBehavior::onBlockGenerated(int32_t value, int32_t x, int32_t y, int32_t z, bool isLoaded)
    {
        addMeter(value, x, y, z);
    }
    void SubsystemMetersBlockBehavior::onBlockAdded(int32_t value, int32_t oldValue, int32_t x, int32_t y, int32_t z)
    {
        addMeter(value, x, y, z);
    }
    void SubsystemMetersBlockBehavior::onBlockRemoved(int32_t value, int32_t newValue, int32_t x, int32_t y, int32_t z)
    {
        removeMeter(newValue, x, y, z);
    }
    void SubsystemMetersBlockBehavior::onBlockModified(int32_t value, int32_t oldValue, int32_t x, int32_t y, int32_t z)
    {
        removeMeter(oldValue, x, y, z);
        addMeter(value, x, y, z);
    }
    void SubsystemMetersBlockBehavior::onChunkDiscarding(TerrainChunk *chunk)
    {
        std::vector<Point3> list;
        m_thermometersByPointMutex.lock();
        for (auto it = m_thermometersByPoint.begin(); it != m_thermometersByPoint.end(); ++it)
        {
            if (it->first.posX >= chunk->origin.posX &&
                it->first.posX < chunk->origin.posX + 16 &&
                it->first.posZ >= chunk->origin.posY &&
                it->first.posZ < chunk->origin.posY + 16)
            {
                list.push_back(it->first);
            }
        }
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            auto pointIt = m_thermometersByPoint.find(*it);
            if (pointIt != m_thermometersByPoint.end())
            {
                m_thermometersByPoint.erase(pointIt);
            }
        }
        m_thermometersByPointMutex.unlock();
    }



    int32_t SubsystemMetersBlockBehavior::getThermometerReading(const Point3 &point)
    {
        std::lock_guard<std::mutex> lk(m_thermometersByPointMutex);
        auto it = m_thermometersByPoint.find(point);
        if (it != m_thermometersByPoint.end())
        {
            return it->second;
        }
        return 0;
    }

    void SubsystemMetersBlockBehavior::calculateTemperature(int32_t x, int32_t y, int32_t z, float meterTemperature, float meterInsulation, float &outTemperature, float &outTemperatureFlux)
    {
        std::vector<int32_t> toVisit;
        int32_t visited[8192] = {0};
        float num  = 0.0f;
        float num2 = 0.0f;
        float num3 = 0.0f;
        float num4 = 0.0f;
        float num5 = 0.0f;
        float num6 = 0.0f;
        toVisit.push_back(133152);
        for (int32_t j = 0; j < toVisit.size(); j++)
        {
            int32_t num7 = toVisit[j];
            if ((visited[num7 / 32] & (1 << num7)) != 0)
            {
                continue;
            }
            visited[num7 / 32] |= 1 << num7;
            int32_t num8 = (num7 & 0x3F) - 32;
            int32_t num9 = ((num7 >> 6) & 0x3F) - 32;
            int32_t num10 = ((num7 >> 12) & 0x3F) - 32;
            int32_t num11 = num8 + x;
            int32_t num12 = num9 + y;
            int32_t num13 = num10 + z;
            TerrainChunk *chunkAtCell = m_terrain->getChunkAtCell(num11, num13);
            if (chunkAtCell == nullptr || num12 < 0 || num12 >= 256)
            {
                continue;
            }
            int32_t x2 = num11 & 0xF;
            int32_t y2 = num12;
            int32_t z2 = num13 & 0xF;
            int32_t cellValueFast = chunkAtCell->getCellValueFast(x2, y2, z2);
            int32_t num14 = Terrain::ExtractContents(cellValueFast);
            Block *block = GameSingleton::blocksManager->blocks[num14];
            float heat = getHeat(cellValueFast);
            if (heat > 0.0f)
            {
                int32_t num15 = MathUtils::Abs<int32_t>(num8) + MathUtils::Abs<int32_t>(num9) + MathUtils::Abs<int32_t>(num10);
                int32_t num16 = (num15 <= 0) ? 1 : (4 * num15 * num15 + 2);
                float num17 = 1.0f / num16;
                num5 += num17 * 36.0f * heat;
                num6 += num17;
            }
            else if (block->isHeatBlocker(cellValueFast))
            {
                int32_t num18 = MathUtils::Abs(num8) + MathUtils::Abs(num9) + MathUtils::Abs(num10);
                int32_t num19 = (num18 <= 0) ? 1 : (4 * num18 * num18 + 2);
                float num20 = 1.0f / num19;
                float num21 = m_terrain->getSeasonTemperatureValue();
                float num22 = SubsystemWeather::GetTemperatureAdjustmentAtHeight(y2);
                float num23 = (block->isBlock<WaterBlock*>()) ? (MathUtils::Max(chunkAtCell->getTemperatureFast(x2, z2) + num21 - 6.0f, 0.0f) + num22) : ((!(block->isBlock<IceBlock*>())) ? (chunkAtCell->getTemperatureFast(x2, z2) + num21 + num22) : (0.0f + num21 + num22));
                num += num20 * num23;
                num2 += num20;
            }
            else if (y >= chunkAtCell->getTopHeightFast(x2, z2))
            {
                int32_t num24 = MathUtils::Abs(num8) + MathUtils::Abs(num9) + MathUtils::Abs(num10);
                int32_t num25 = (num24 <= 0) ? 1 : (4 * num24 * num24 + 2);
                float num26 = 1.0f / num25;
                PrecipitationShaftInfo precipitationShaftInfo = m_weather->getPrecipitationShaftInfo(x, z);
                float num27 = m_terrain->getSeasonTemperatureValue();
                float num28 = (y >= precipitationShaftInfo.YLimit) ? MathUtils::Lerp(0.0f, -2.0f, precipitationShaftInfo.Intensity) : 0.0f;
                float num29 = MathUtils::Lerp(-6.0f, 0.0f, m_weather->skylightIntensity);
                float num30 = SubsystemWeather::GetTemperatureAdjustmentAtHeight(y2);
                num3 += num26 * (chunkAtCell->getTemperatureFast(x2, z2) + num27 + num28 + num29 + num30);
                num4 += num26;
            }
            else if (toVisit.size() < 4090)
            {
                if (num8 > -30)
                {
                    toVisit.push_back(num7 - 1);
                }
                if (num8 < 30)
                {
                    toVisit.push_back(num7 + 1);
                }
                if (num9 > -30)
                {
                    toVisit.push_back(num7 - 64);
                }
                if (num9 < 30)
                {
                    toVisit.push_back(num7 + 64);
                }
                if (num10 > -30)
                {
                    toVisit.push_back(num7 - 4096);
                }
                if (num10 < 30)
                {
                    toVisit.push_back(num7 + 4096);
                }
            }
        }
        float num31 = 0.0f;
        for (int32_t k = -7; k <= 7; k++)
        {
            for (int32_t l = -7; l <= 7; l++)
            {
                TerrainChunk *chunkAtCell2 = m_terrain->getChunkAtCell(x + k, z + l);
                if (chunkAtCell2 == nullptr || chunkAtCell2->State < TerrainChunkState::InvalidVertices1)
                {
                    continue;
                }
                for (int32_t m = -7; m <= 7; m++)
                {
                    int32_t num32 = k * k + m * m + l * l;
                    if (num32 > 49 || num32 <= 0)
                    {
                        continue;
                    }
                    int32_t x3 = (x + k) & 0xF;
                    int32_t num33 = y + m;
                    int32_t z3 = (z + l) & 0xF;
                    if (num33 >= 0 && num33 < 256)
                    {
                        float heat2 = getHeat(chunkAtCell2->getCellValueFast(x3, num33, z3));
                        Vector3 endVector3(x + k + 0.5f, y + m + 0.75f, z + l + 0.5f);
                        if (heat2 > 0.0f && 
                            nullptr == m_terrain->raycast(Vector3(x + 0.5f, y + 0.75f, z + 0.5f), endVector3, false, true, [this](int32_t raycastValue, float d) -> bool {
                                                                   Block *block2 = GameSingleton::blocksManager->blocks[Terrain::ExtractContents(raycastValue)];
                                                                   return block2->IsCollidable && !block2->IsTransparent;
                                                               }))
                        {
                            num31 += heat2 * 3.0f / (num32 + 2);
                        }
                    }
                }
            }
        }
        float num34 = 0.0f;
        float num35 = 0.0f;
        if (num31 > 0.0f)
        {
            float num36 = 3.0f * num31;
            num34 += 35.0f * num36;
            num35 += num36;
        }
        if (num2 > 0.0f)
        {
            float num37 = 1.0f;
            num34 += num / num2 * num37;
            num35 += num37;
        }
        if (num4 > 0.0f)
        {
            float num38 = 4.0f * MathUtils::Pow(num4, 0.25f);
            num34 += num3 / num4 * num38;
            num35 += num38;
        }
        if (num6 > 0.0f)
        {
            float num39 = 1.5f * MathUtils::Pow(num6, 0.25f);
            num34 += num5 / num6 * num39;
            num35 += num39;
        }
        if (meterInsulation > 0.0f)
        {
            num34 += meterTemperature * meterInsulation;
            num35 += meterInsulation;
        }
        outTemperature = (num35 > 0.0f) ? (num34 / num35) : meterTemperature;
        outTemperatureFlux = num35 - meterInsulation;
    }

    float SubsystemMetersBlockBehavior::getHeat(int32_t value)
    {
        int32_t num = Terrain::ExtractContents(value);
        return GameSingleton::blocksManager->blocks[num]->getHeat(value);
    }

    void SubsystemMetersBlockBehavior::simulateThermometer(int32_t x, int32_t y, int32_t z, bool invalidateTerrainOnChange)
    {
        Point3 point(x, y, z);

        m_thermometersByPointMutex.lock();

        auto it = m_thermometersByPoint.find(point);
        if (it != m_thermometersByPoint.end())
        {
            float temperature;
            float temperatureFlux;
            calculateTemperature(x, y, z, 0.0f, 0.0f, temperature, temperatureFlux);
            int32_t num2 = MathUtils::Clamp<int32_t>(MathUtils::Round(temperature), 0, 15);
            if (num2 != it->second)
            {
                it->second = num2;
                // if(invalidateTerrainOnChange == true)
                // {
                //     TerrainChunk *chunk = m_terrain->getChunkAtCell(x, z);
                //     if(chunk != nullptr)
                //     {
                //         chunk->State = TerrainChunkState::InvalidVertices1;
                //     }
                // }
            }
        }

        m_thermometersByPointMutex.unlock();
    }

    void SubsystemMetersBlockBehavior::addMeter(int32_t value, int32_t x, int32_t y, int32_t z)
    {
        if (Terrain::ExtractContents(value) == ThermometerBlock::Index())
        {
            m_thermometersByPointMutex.lock();
            m_thermometersByPoint.emplace(Point3(x, y, z), 0);
            m_thermometersByPointMutex.unlock();
            simulateThermometer(x, y, z, false);
        }
    }

    void SubsystemMetersBlockBehavior::removeMeter(int32_t value, int32_t x, int32_t y, int32_t z)
    {
        m_thermometersByPointMutex.lock();
        auto it = m_thermometersByPoint.find(Point3(x, y, z));
        if (it != m_thermometersByPoint.end())
        {
            m_thermometersByPoint.erase(it);
        }
        m_thermometersByPointMutex.unlock();
    }



    void SubsystemMetersBlockBehavior::update(float dt)
    {
        if (m_thermometersToSimulateIndex < m_thermometersToSimulate.size())
        {
            double period = MathUtils::Max(5.0 / m_thermometersToSimulate.size(), 1.0);
            if (m_timeOfDay->periodicGameTimeEvent(period, 0.0))
            {
                Point3 point = m_thermometersToSimulate[m_thermometersToSimulateIndex];
                simulateThermometer(point.posX, point.posY, point.posZ, true);
                m_thermometersToSimulateIndex++;
            }
        }
        else
        {
            m_thermometersByPointMutex.lock();
            if (m_thermometersByPoint.size() > 0)
            {
                m_thermometersToSimulateIndex = 0;
                m_thermometersToSimulate.clear();
                for (auto it = m_thermometersByPoint.begin(); it != m_thermometersByPoint.end(); ++it)
                {
                    m_thermometersToSimulate.push_back(it->first);
                }
            }
            m_thermometersByPointMutex.unlock();
        }
    }

    void SubsystemMetersBlockBehavior::load(const nlohmann::json &json)
    {
        m_weather = GameSingleton::singletonPtr->getSingleton<SubsystemWeather>();
        m_timeOfDay = GameSingleton::singletonPtr->getSingleton<SubsystemTimeOfDay>();
        m_terrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
    }

    static const std::string subsystemName = "MetersBlockBehavior";
    const std::string &SubsystemMetersBlockBehavior::getName() const
    {
        return subsystemName;
    }
    SubUpdateType SubsystemMetersBlockBehavior::getUpdateType()
    {
        return SubUpdateType::MultiThreadUpdate;
    }
}